import { Point, toPoint } from './Point';

/*
 * @class Bounds
 * @aka L.Bounds
 *
 * Represents a rectangular area in pixel coordinates.
 *
 * @example
 *
 * ```js
 * var p1 = L.point(10, 10),
 * p2 = L.point(40, 60),
 * bounds = L.bounds(p1, p2);
 * ```
 *
 * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:
 *
 * ```js
 * otherBounds.intersects([[10, 10], [40, 60]]);
 * ```
 *
 * Note that `Bounds` does not inherit from Leaflet's `Class` object,
 * which means new classes can't inherit from it, and new methods
 * can't be added to it with the `include` function.
 */

export function Bounds(a, b) {
  if (!a) {
    return;
  }

  var points = b ? [a, b] : a;

  for (var i = 0, len = points.length; i < len; i++) {
    this.extend(points[i]);
  }
}

Bounds.prototype = {
  // @method extend(point: Point): this
  // Extends the bounds to contain the given point.

  // @alternative
  // @method extend(otherBounds: Bounds): this
  // Extend the bounds to contain the given bounds
  extend: function (obj) {
    var min2, max2;
    if (!obj) {
      return this;
    }

    if (obj instanceof Point || typeof obj[0] === 'number' || 'x' in obj) {
      min2 = max2 = toPoint(obj);
    } else {
      obj = toBounds(obj);
      min2 = obj.min;
      max2 = obj.max;

      if (!min2 || !max2) {
        return this;
      }
    }

    // @property min: Point
    // The top left corner of the rectangle.
    // @property max: Point
    // The bottom right corner of the rectangle.
    if (!this.min && !this.max) {
      this.min = min2.clone();
      this.max = max2.clone();
    } else {
      this.min.x = Math.min(min2.x, this.min.x);
      this.max.x = Math.max(max2.x, this.max.x);
      this.min.y = Math.min(min2.y, this.min.y);
      this.max.y = Math.max(max2.y, this.max.y);
    }
    return this;
  },

  // @method getCenter(round?: Boolean): Point
  // Returns the center point of the bounds.
  getCenter: function (round) {
    return toPoint(
      (this.min.x + this.max.x) / 2,
      (this.min.y + this.max.y) / 2,
      round,
    );
  },

  // @method getBottomLeft(): Point
  // Returns the bottom-left point of the bounds.
  getBottomLeft: function () {
    return toPoint(this.min.x, this.max.y);
  },

  // @method getTopRight(): Point
  // Returns the top-right point of the bounds.
  getTopRight: function () {
    // -> Point
    return toPoint(this.max.x, this.min.y);
  },

  // @method getTopLeft(): Point
  // Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).
  getTopLeft: function () {
    return this.min; // left, top
  },

  // @method getBottomRight(): Point
  // Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).
  getBottomRight: function () {
    return this.max; // right, bottom
  },

  // @method getSize(): Point
  // Returns the size of the given bounds
  getSize: function () {
    return this.max.subtract(this.min);
  },

  // @method contains(otherBounds: Bounds): Boolean
  // Returns `true` if the rectangle contains the given one.
  // @alternative
  // @method contains(point: Point): Boolean
  // Returns `true` if the rectangle contains the given point.
  contains: function (obj) {
    var min, max;

    if (typeof obj[0] === 'number' || obj instanceof Point) {
      obj = toPoint(obj);
    } else {
      obj = toBounds(obj);
    }

    if (obj instanceof Bounds) {
      min = obj.min;
      max = obj.max;
    } else {
      min = max = obj;
    }

    return (
      min.x >= this.min.x &&
      max.x <= this.max.x &&
      min.y >= this.min.y &&
      max.y <= this.max.y
    );
  },

  // @method intersects(otherBounds: Bounds): Boolean
  // Returns `true` if the rectangle intersects the given bounds. Two bounds
  // intersect if they have at least one point in common.
  intersects: function (bounds) {
    // (Bounds) -> Boolean
    bounds = toBounds(bounds);

    var min = this.min,
      max = this.max,
      min2 = bounds.min,
      max2 = bounds.max,
      xIntersects = max2.x >= min.x && min2.x <= max.x,
      yIntersects = max2.y >= min.y && min2.y <= max.y;

    return xIntersects && yIntersects;
  },

  // @method overlaps(otherBounds: Bounds): Boolean
  // Returns `true` if the rectangle overlaps the given bounds. Two bounds
  // overlap if their intersection is an area.
  overlaps: function (bounds) {
    // (Bounds) -> Boolean
    bounds = toBounds(bounds);

    var min = this.min,
      max = this.max,
      min2 = bounds.min,
      max2 = bounds.max,
      xOverlaps = max2.x > min.x && min2.x < max.x,
      yOverlaps = max2.y > min.y && min2.y < max.y;

    return xOverlaps && yOverlaps;
  },

  // @method isValid(): Boolean
  // Returns `true` if the bounds are properly initialized.
  isValid: function () {
    return !!(this.min && this.max);
  },

  // @method pad(bufferRatio: Number): Bounds
  // Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.
  // For example, a ratio of 0.5 extends the bounds by 50% in each direction.
  // Negative values will retract the bounds.
  pad: function (bufferRatio) {
    var min = this.min,
      max = this.max,
      heightBuffer = Math.abs(min.x - max.x) * bufferRatio,
      widthBuffer = Math.abs(min.y - max.y) * bufferRatio;

    return toBounds(
      toPoint(min.x - heightBuffer, min.y - widthBuffer),
      toPoint(max.x + heightBuffer, max.y + widthBuffer),
    );
  },

  // @method equals(otherBounds: Bounds): Boolean
  // Returns `true` if the rectangle is equivalent to the given bounds.
  equals: function (bounds) {
    if (!bounds) {
      return false;
    }

    bounds = toBounds(bounds);

    return (
      this.min.equals(bounds.getTopLeft()) &&
      this.max.equals(bounds.getBottomRight())
    );
  },
};

// @factory L.bounds(corner1: Point, corner2: Point)
// Creates a Bounds object from two corners coordinate pairs.
// @alternative
// @factory L.bounds(points: Point[])
// Creates a Bounds object from the given array of points.
export function toBounds(a, b) {
  if (!a || a instanceof Bounds) {
    return a;
  }
  return new Bounds(a, b);
}
